package classfile

import (
	"archive/zip"
	"errors"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
)

type Entity interface {
	Read(class string) ([]byte, Entity, error)
}

type ZipEntity struct { // xxx.zip, xxx.jar, xxx.ZIP, xxx.JAR
	file string
}

type DirEntity struct { // %dir%/*.class
	dir string
}

func (d DirEntity) String() string {
	return "DirEntity: " + d.dir + "/*.class"
}

type CompositeEntity []Entity

func newZipEntity(src string) *ZipEntity {
	return &ZipEntity{file: src}
}

func (d ZipEntity) String() string {
	return "ZipEntity: " + d.file
}

func (d *ZipEntity) Read(class string) ([]byte, Entity, error) {
	npath := strings.ReplaceAll(class, ".", "/")
	zr, err := zip.OpenReader(d.file)
	if err != nil {
		return nil, nil, err
	}
	defer zr.Close()

	r, e := zr.Open(npath + ".class")
	if e != nil {
		return nil, nil, e
	}
	defer r.Close()
	bt, e := ioutil.ReadAll(r)
	if e == nil {
		return bt, d, nil
	}
	return nil, nil, errors.New("Not find class " + class)
}

func newDirEntity(src string) *DirEntity {
	return &DirEntity{dir: src}
}

func (d *DirEntity) Read(class string) ([]byte, Entity, error) {
	// filepath.Join
	class = strings.ReplaceAll(class, ".", Separator) + ".class"
	path := filepath.Join(d.dir, class)
	f, e := os.Open(path)
	defer f.Close()

	if e != nil {
		return nil, nil, e
	}
	bts, e := ioutil.ReadAll(f)
	if e != nil {
		return nil, nil, e
	}
	return bts, d, nil
}

func ParseEntity(classpath string) Entity {
	if strings.Contains(classpath, ListSeparator) {
		return newCompositeEntity(classpath)
	} else if strings.HasSuffix(classpath, ".jar") {
		return newZipEntity(classpath)
	} else if strings.HasSuffix(classpath, ".JAR") {
		return newZipEntity(classpath)
	} else if strings.HasSuffix(classpath, ".zip") {
		return newZipEntity(classpath)
	} else if strings.HasSuffix(classpath, ".ZIP") {
		return newZipEntity(classpath)
	}
	return newDirEntity(classpath)
}

func newCompositeEntity(src string) *CompositeEntity {
	ss := strings.Split(src, ListSeparator)
	ts := make(CompositeEntity, 0, len(ss))
	for _, s := range ss {
		ts = append(ts, ParseEntity(s))
	}
	return &ts
}

func (com *CompositeEntity) Read(class string) ([]byte, Entity, error) {
	for _, en := range *com {
		bts, entity, e := en.Read(class)
		if e == nil {
			return bts, entity, e
		}
	}
	return nil, nil, errors.New("Not find class " + class)
}
